﻿<script setup lang="ts" name="VueSeamlessScroll">
import type { CSSProperties, PropType } from 'vue'
import { animationFrame, copyObj } from './utils'
import { transformI18n } from '@/plugins/i18n'
import {
  tryOnMounted,
  tryOnUnmounted,
  templateRef,
  useDebounceFn
} from '@vueuse/core'

animationFrame()

const props = defineProps({
  data: {
    type: Array as PropType<unknown>
  },
  customOption: {
    type: Object as PropType<unknown>
  }
})

const emit = defineEmits<{(e: 'scrollEnd'): void}>()

const xPos = ref<number>(0)
const yPos = ref<number>(0)
const delay = ref<number>(0)
const height = ref<number>(0)
// 外容器宽度
const width = ref<number>(0)
// 内容实际宽度
const realBoxWidth = ref<number>(0)
const realBoxHeight = ref<number>(0)
const copyHtml = ref('')
// single 单步滚动的定时器
let singleWaitTime = null
// move动画的animationFrame定时器
let reqFrame = null
let startPos = null
// 记录touchStart时候的posY
let startPosY = null
// 记录touchStart时候的posX
let startPosX = null
// mouseenter mouseleave 控制scrollMove()的开关
let isHover = false
let ease = 'ease-in'

// eslint-disable-next-line vue/no-setup-props-destructure
const { customOption } = props

if (customOption['key'] === undefined) {
  customOption['key'] = 0
}

const wrap = templateRef<HTMLElement | null>(`wrap${customOption['key']}`, null)
const slotList = templateRef<HTMLElement | null>(
  `slotList${customOption['key']}`,
  null
)
const realBox = templateRef<HTMLElement | null>(
  `realBox${customOption['key']}`,
  null
)

const leftSwitchState = computed(() => {
  if (unref(isHorizontal)) {
    return unref(xPos) < 0
  } else {
    return unref(yPos) < 0
  }
})

const rightSwitchState = computed(() => {
  if (unref(isHorizontal)) {
    return Math.abs(unref(xPos)) < unref(realBoxWidth) - unref(width)
  } else {
    return Math.abs(unref(yPos)) < unref(realBoxHeight) - unref(height)
  }
})

const defaultOption = computed(() => {
  return {
    // 步长 数值越大滚动越快
    step: 1,
    // 启动无缝滚动最小数据数
    limitMoveNum: 5,
    // 是否启用鼠标hover控制
    hoverStop: true,
    // bottom 往下 top 往上(默认) left 向左 right 向右
    direction: 'top',
    // 开启移动端touch
    openTouch: true,
    // 单条数据高度有值hoverStop关闭
    singleHeight: 0,
    // 单条数据宽度有值hoverStop关闭
    singleWidth: 0,
    // 单步停止等待时间
    waitTime: 1000,
    switchOffset: 30,
    autoPlay: true,
    navigation: false,
    switchSingleStep: 134,
    switchDelay: 400,
    switchDisabledClass: 'disabled',
    // singleWidth/singleHeight 是否开启rem度量
    isSingleRemUnit: false
  }
})

const options = computed(() => {
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
  // @ts-ignore
  return copyObj({}, unref(defaultOption), customOption)
})

const leftSwitchClass = computed(() => {
  return unref(leftSwitchState) ? '' : unref(options).switchDisabledClass
})

const rightSwitchClass = computed(() => {
  return unref(rightSwitchState) ? '' : unref(options).switchDisabledClass
})

const isHorizontal = computed(() => {
  return (
    unref(options).direction !== 'bottom' && unref(options).direction !== 'top'
  )
})

const float = computed((): CSSProperties => {
  return unref(isHorizontal)
    ? { float: 'left', overflow: 'hidden' }
    : { overflow: 'hidden' }
})

const pos = computed(() => {
  return {
    transform: `translate(${unref(xPos)}px,${unref(yPos)}px)`,
    transition: `all ${ease} ${unref(delay)}ms`,
    overflow: 'hidden'
  }
})

const navigation = computed(() => {
  return unref(options).navigation
})

const autoPlay = computed(() => {
  if (unref(navigation)) return false
  return unref(options).autoPlay
})

const scrollSwitch = computed(() => {
  // 从 props 解构出来的 属性 不再具有相应性.
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
  // @ts-expect-error
  return props.data.length >= unref(options).limitMoveNum
})

const hoverStopSwitch = computed(() => {
  return unref(options).hoverStop && unref(autoPlay) && unref(scrollSwitch)
})

const canTouchScroll = computed(() => {
  return unref(options).openTouch
})

const baseFontSize = computed(() => {
  return unref(options).isSingleRemUnit
    ? parseInt(window.getComputedStyle(document.documentElement, null).fontSize)
    : 1
})

const realSingleStopWidth = computed(() => {
  return unref(options).singleWidth * unref(baseFontSize)
})

const realSingleStopHeight = computed(() => {
  return unref(options).singleHeight * unref(baseFontSize)
})

const step = computed(() => {
  let singleStep
  const step = unref(options).step
  if (unref(isHorizontal)) {
    singleStep = unref(realSingleStopWidth)
  } else {
    singleStep = unref(realSingleStopHeight)
  }
  if (singleStep > 0 && singleStep % step > 0) {
    // eslint-disable-next-line no-throw-literal
    throw new Error(transformI18n('errors.seamlessScroll', true))
  }
  return step
})

const reset = () => {
  xPos.value = 0
  yPos.value = 0
  scrollCancle()
  scrollInitMove()
}

function leftSwitchClick() {
  if (!unref(leftSwitchState)) return
  // 小于单步距离
  if (unref(isHorizontal)) {
    if (Math.abs(unref(xPos)) < unref(options).switchSingleStep) {
      xPos.value = 0
      return
    }
    xPos.value += unref(options).switchSingleStep
  } else {
    if (Math.abs(unref(yPos)) < unref(options).switchSingleStep) {
      yPos.value = 0
      return
    }
    yPos.value += unref(options).switchSingleStep
  }
}

function rightSwitchClick() {
  console.log(unref(rightSwitchState))
  if (!unref(rightSwitchState)) return
  // 小于单步距离
  if (unref(isHorizontal)) {
    if (
      unref(realBoxWidth) - unref(width) + unref(xPos) <
      unref(options).switchSingleStep
    ) {
      xPos.value = unref(width) - unref(realBoxWidth)
      return
    }
    xPos.value -= unref(options).switchSingleStep
  } else {
    if (
      unref(realBoxHeight) - unref(height) + unref(yPos) <
      unref(options).switchSingleStep
    ) {
      yPos.value = unref(height) - unref(realBoxHeight)
      return
    }
    yPos.value -= unref(options).switchSingleStep
  }
}

function scrollCancle() {
  cancelAnimationFrame(reqFrame || '')
}

function touchStart(e) {
  if (!unref(canTouchScroll)) return
  let timer
  // touches数组对象获得屏幕上所有的touch，取第一个touch
  const touch = e.targetTouches[0]
  const { waitTime, singleHeight, singleWidth } = unref(options)
  // 取第一个touch的坐标值
  startPos = {
    x: touch.pageX,
    y: touch.pageY
  }
  // 记录touchStart时候的posY
  startPosY = unref(yPos)
  // 记录touchStart时候的posX
  startPosX = unref(xPos)
  if (!!singleHeight && !!singleWidth) {
    if (timer) clearTimeout(timer)
    timer = setTimeout(() => {
      scrollCancle()
    }, waitTime + 20)
  } else {
    scrollCancle()
  }
}

function touchMove(e) {
  // 当屏幕有多个touch或者页面被缩放过，就不执行move操作
  if (
    !unref(canTouchScroll) ||
    e.targetTouches.length > 1 ||
    (e.scale && e.scale !== 1)
  ) { return }
  const touch = e.targetTouches[0]
  const { direction } = unref(options)
  const endPos = {
    x: touch.pageX - startPos.x,
    y: touch.pageY - startPos.y
  }
  // 阻止触摸事件的默认行为，即阻止滚屏
  e.preventDefault()
  // dir，1表示纵向滑动，0为横向滑动
  const dir = Math.abs(endPos.x) < Math.abs(endPos.y) ? 1 : 0
  if (
    (dir === 1 && direction === 'bottom') ||
    (dir === 1 && direction === 'top')
  ) {
    // 表示纵向滑动 && 运动方向为上下
    yPos.value = startPosY + endPos.y
  } else if (
    (dir === 0 && direction === 'left') ||
    (dir === 0 && direction === 'right')
  ) {
    // 为横向滑动 && 运动方向为左右
    xPos.value = startPosX + endPos.x
  }
}

function touchEnd() {
  if (!unref(canTouchScroll)) return
  let timer = null
  const direction = unref(options).direction
  delay.value = 50
  if (direction === 'top') {
    if (unref(yPos) > 0) yPos.value = 0
  } else if (direction === 'bottom') {
    const h = (unref(realBoxHeight) / 2) * -1
    if (unref(yPos) < h) yPos.value = h
  } else if (direction === 'left') {
    if (unref(xPos) > 0) xPos.value = 0
  } else if (direction === 'right') {
    const w = unref(realBoxWidth) * -1
    if (unref(xPos) < w) xPos.value = w
  }
  if (timer) clearTimeout(timer)
  timer = setTimeout(() => {
    delay.value = 0
    scrollMove()
  }, unref(delay))
}

const enter = () => {
  if (unref(hoverStopSwitch)) scrollStopMove()
}

const leave = () => {
  if (unref(hoverStopSwitch)) scrollStartMove()
}

const scrollMove = () => {
  // 鼠标移入时拦截scrollMove()
  if (isHover) return
  // 进入move立即先清除动画 防止频繁touchMove导致多动画同时进行
  // scrollCancle();
  reqFrame = requestAnimationFrame(function () {
    // 实际高度
    const h = unref(realBoxHeight) / 2
    // 宽度
    const w = unref(realBoxWidth) / 2
    const { direction, waitTime } = unref(options)
    if (direction === 'top') {
      // 上
      if (Math.abs(unref(yPos)) >= h) {
        emit('scrollEnd')
        yPos.value = 0
      }
      yPos.value -= step.value
    } else if (direction === 'bottom') {
      // 下
      if (unref(yPos) >= 0) {
        emit('scrollEnd')
        yPos.value = h * -1
      }
      yPos.value += step.value
    } else if (direction === 'left') {
      // 左
      if (Math.abs(unref(xPos)) >= w) {
        emit('scrollEnd')
        xPos.value = 0
      }
      xPos.value -= step.value
    } else if (direction === 'right') {
      // 右
      if (unref(xPos) >= 0) {
        emit('scrollEnd')
        xPos.value = w * -1
      }
      xPos.value += step.value
    }
    if (singleWaitTime) clearTimeout(singleWaitTime)
    if (unref(realSingleStopHeight)) {
      // 是否启动了单行暂停配置
      if (Math.abs(unref(yPos)) % unref(realSingleStopHeight) < unref(step)) {
        // 符合条件暂停waitTime
        singleWaitTime = setTimeout(() => {
          scrollMove()
        }, waitTime)
      } else {
        scrollMove()
      }
    } else if (unref(realSingleStopWidth)) {
      if (Math.abs(unref(xPos)) % unref(realSingleStopWidth) < unref(step)) {
        // 符合条件暂停waitTime
        singleWaitTime = setTimeout(() => {
          scrollMove()
        }, waitTime)
      } else {
        scrollMove()
      }
    } else {
      scrollMove()
    }
  })
}

const scrollInitMove = () => {
  nextTick(() => {
    const { switchDelay } = unref(options)
    // 清空copy
    copyHtml.value = ''
    if (unref(isHorizontal)) {
      height.value = unref(wrap).offsetHeight
      width.value = unref(wrap).offsetWidth
      let slotListWidth = unref(slotList).offsetWidth
      // 水平滚动设置warp width
      if (unref(autoPlay)) {
        // 修正offsetWidth四舍五入
        slotListWidth = slotListWidth * 2 + 1
      }
      unref(realBox).style.width = slotListWidth + 'px'
      realBoxWidth.value = slotListWidth
    } else {
      height.value = unref(wrap).offsetHeight
      width.value = unref(wrap).offsetWidth
      let slotListHeight = unref(slotList).offsetHeight
      // 水平滚动设置warp width
      if (unref(autoPlay)) {
        // 修正offsetWidth四舍五入
        slotListHeight = slotListHeight * 2 + 1
      }
      unref(realBox).style.height = slotListHeight + 'px'
      realBoxHeight.value = slotListHeight
    }

    if (unref(autoPlay)) {
      ease = 'ease-in'
      delay.value = 0
    } else {
      ease = 'linear'
      delay.value = switchDelay
      return
    }

    // 是否可以滚动判断
    if (unref(scrollSwitch)) {
      let timer
      if (timer) clearTimeout(timer)
      copyHtml.value = unref(slotList).innerHTML
      setTimeout(() => {
        realBoxHeight.value = unref(realBox).offsetHeight
        scrollMove()
      }, 0)
    } else {
      scrollCancle()
      yPos.value = xPos.value = 0
    }
  })
}

const scrollStartMove = () => {
  // 开启scrollMove
  isHover = false
  scrollMove()
}

const scrollStopMove = () => {
  // 关闭scrollMove
  isHover = true
  // 防止频频hover进出单步滚动,导致定时器乱掉
  if (singleWaitTime) clearTimeout(singleWaitTime)
  scrollCancle()
}

// 鼠标滚轮事件
const wheel = (e) => {
  e.preventDefault()
  if (
    unref(options).direction === 'left' ||
    unref(options).direction === 'right'
  ) { return }
  useDebounceFn(() => {
    e.deltaY > 0 ? (yPos.value -= step.value) : (yPos.value += step.value)
  }, 50)()
}

tryOnMounted(() => {
  scrollInitMove()
})

tryOnUnmounted(() => {
  scrollCancle()
  clearTimeout(singleWaitTime)
})

defineExpose({
  reset
})
</script>

<template>
  <div class="seamless-wrap" :ref="'wrap' + customOption['key']">
    <div v-if="navigation" :class="['prev', leftSwitchClass]" @click="leftSwitchClick">
      <slot name="left-switch" />
    </div>
    <div v-if="navigation" :class="['next', rightSwitchClass]" @click="rightSwitchClick">
      <slot name="right-switch" />
    </div>
    <div class="seamless-content">
      <div class="seamless-box" :ref="'realBox' + customOption['key']" :style="pos" @mouseenter="enter" @mouseleave="leave" @touchstart="touchStart" @touchmove="touchMove" @touchend="touchEnd" @mousewheel="wheel">
        <div class="seamless-default" :ref="'slotList' + customOption['key']" :style="float">
          <slot />
        </div>
        <div class="seamless-copy" v-html="copyHtml" :style="float" />
      </div>
    </div>
  </div>
</template>

<style scoped lang="scss">
.seamless-wrap{
  position: relative;

  .prev,.next{
    position: absolute;
  }

  .seamless-content{
    position: relative;
    width: 100%;
    height: 100%;
    overflow: hidden;
  }

  .seamless-box{
    position: absolute;
    top: 0;
    left: 0;
  }
}
</style>
